// File: route.ts // Path: apps/web/app/api/v1/occupations/[code]/route.ts // Project: AI Risk Index — airiskindex.io // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // // Description: Public API: full score breakdown for one occupation. import { prisma } from "@airiskindex/db"; import { INDEX_VERSION } from "@airiskindex/scoring"; import type { NextRequest } from "next/server"; export const dynamic = "force-dynamic"; export async function GET( _request: NextRequest, { params }: { params: { code: string } }, ): Promise { try { const occupation = await prisma.occupation.findUnique({ where: { code: params.code }, include: { tasks: { select: { id: true, statement: true, importance: true }, orderBy: { id: "asc" }, }, }, }); if (!occupation) { return Response.json({ error: "not_found" }, { status: 404 }); } // Latest run's scores; historical runs stay queryable (CLAUDE.md §9). const latest = await prisma.occupationScore.findFirst({ where: { occupationCode: occupation.code }, orderBy: { run: { createdAt: "desc" } }, include: { run: true }, }); return Response.json({ index_version: latest?.run.indexVersion ?? INDEX_VERSION, occupation: { code: occupation.code, title: occupation.title, description: occupation.description, esco_uri: occupation.escoUri, rome_code: occupation.romeCode, median_wage_cents: occupation.medianWageCents, wage_currency: occupation.wageCurrency, }, scores: latest ? { run_id: latest.runId, computed_at: latest.run.createdAt, substitution: { low: latest.substitutionLow, score: latest.substitution, high: latest.substitutionHigh, }, exposure: { low: latest.exposureLow, score: latest.exposure, high: latest.exposureHigh, }, augmentation: { low: latest.augmentationLow, score: latest.augmentation, high: latest.augmentationHigh, }, highly_exposed_task_share: latest.highlyExposedTaskShare, } : null, tasks: occupation.tasks, }); } catch { return Response.json({ error: "database_unavailable" }, { status: 503 }); } }